﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Aeon.Emulator.DebugSupport;
using Aeon.Emulator.Decoding.Recompiler;
using Aeon.Emulator.RuntimeExceptions;
using Aeon.Emulator.Dos.Programs;
using Aeon.Emulator.Interrupts;

namespace Aeon.Emulator
{
    /// <summary>
    /// Hosts a VirtualMachine instance and provides additional services.
    /// </summary>
    public sealed class EmulatorHost : IDisposable
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the EmulatorHost class.
        /// </summary>
        public EmulatorHost()
            : this(FastVirtualMachine.Create(), null)
        {
        }
        /// <summary>
        /// Initializes a new instance of the EmulatorHost class.
        /// </summary>
        /// <param name="instructionLog">Log which will be used to record instructions.</param>
        [CLSCompliant(false)]
        public EmulatorHost(InstructionLog instructionLog)
            : this(FastVirtualMachine.Create(), instructionLog)
        {
        }
        /// <summary>
        /// Initializes a new instance of the EmulatorHost class.
        /// </summary>
        /// <param name="virtualMachine">VirtualMachine instance to host.</param>
        /// <param name="instructionLog">Log which will be used to record instructions.</param>
        [CLSCompliant(false)]
        public EmulatorHost(VirtualMachine virtualMachine, InstructionLog instructionLog)
        {
            if(virtualMachine == null)
                throw new ArgumentNullException("virtualMachine");

            vm = virtualMachine;
            vm.VideoModeChanged += (s, e) => this.OnVideoModeChanged(e);
            vm.MouseMoveByEmulator += (s, e) => this.OnMouseMoveByEmulator(e);
            vm.MouseMove += (s, e) => this.OnMouseMove(e);
            vm.MouseVisibilityChanged += (s, e) => this.OnMouseVisibilityChanged(e);
            vm.CursorVisibilityChanged += (s, e) => this.OnCursorVisibilityChanged(e);
            vm.CurrentProcessChanged += (s, e) => this.OnCurrentProcessChanged(e);

            this.log = instructionLog;
        }
        #endregion

        #region Public Events
        /// <summary>
        /// Occurs when the emulated display mode has changed.
        /// </summary>
        public event EventHandler VideoModeChanged;
        /// <summary>
        /// Occurs when the emulator sets the mouse position.
        /// </summary>
        public event EventHandler<MouseMoveEventArgs> MouseMoveByEmulator;
        /// <summary>
        /// Occurs when the internal mouse position has changed.
        /// </summary>
        public event EventHandler<MouseMoveEventArgs> MouseMove;
        /// <summary>
        /// Occurs when the mouse cursor is shown or hidden.
        /// </summary>
        public event EventHandler MouseVisibilityChanged;
        /// <summary>
        /// Occurs when the text-mode cursor is shown or hidden.
        /// </summary>
        public event EventHandler CursorVisibilityChanged;
        /// <summary>
        /// Occurs when the current process has changed.
        /// </summary>
        public event EventHandler CurrentProcessChanged;
        /// <summary>
        /// Occurs when the emulator state has changed.
        /// </summary>
        public event EventHandler StateChanged;
        /// <summary>
        /// Occurs when the emulator has halted due to an error.
        /// </summary>
        public event EventHandler<ErrorEventArgs> Error;
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets the current state of the emulated system.
        /// </summary>
        public EmulatorState State
        {
            get
            {
                return currentState;
            }
            private set
            {
                if(value != currentState)
                {
                    currentState = value;
                    ThreadPool.QueueUserWorkItem(state => OnStateChanged(EventArgs.Empty));
                }
            }
        }
        /// <summary>
        /// Gets the hosted VirtualMachine instance.
        /// </summary>
        public VirtualMachine VirtualMachine
        {
            get { return vm; }
        }
        /// <summary>
        /// Gets the total number of instructions executed.
        /// </summary>
        public long TotalInstructions
        {
            get { return totalInstructions; }
        }
        /// <summary>
        /// Gets or sets the current emulation speed.
        /// </summary>
        public int EmulationSpeed
        {
            get { return emulationSpeed; }
            set
            {
                if(value < EmulatorHost.MinimumSpeed || value > EmulatorHost.MaximumSpeed)
                    throw new ArgumentOutOfRangeException();

                emulationSpeed = value;
            }
        }
        /// <summary>
        /// Gets or sets the object to use for raising events.
        /// </summary>
        public IEventSynchronizer EventSynchronizer { get; set; }
        #endregion

        #region Public Methods
        /// <summary>
        /// Loads an executable program image into the emulator.
        /// </summary>
        /// <param name="fileName">Name of executable file to load.</param>
        public void LoadProgram(string fileName)
        {
            LoadProgram(fileName, null);
        }
        /// <summary>
        /// Loads an executable program image into the emulator.
        /// </summary>
        /// <param name="fileName">Name of executable file to load.</param>
        /// <param name="commandLineArguments">Command line arguments for the program.</param>
        public void LoadProgram(string fileName, string commandLineArguments)
        {
            if(fileName == null)
                throw new ArgumentNullException("fileName");
            if(commandLineArguments != null && commandLineArguments.Length > 127)
                throw new ArgumentException("Command line length must not exceed 127 characters.", "commandLineArguments");

            vm.EndInitialization();

            var image = ProgramImage.Load(fileName, vm);
            vm.LoadImage(image, commandLineArguments);

            // Sierra adventure games assume a virtual width of 320 for the mouse for some reason.
            string upperFileName = fileName.ToUpper();
            if(upperFileName.Contains("SIERRA.EXE") || upperFileName.Contains("SCIDHUV.EXE"))
                vm.Mouse.UseDefaultVirtualWidth = false;
            else
                vm.Mouse.UseDefaultVirtualWidth = true;

            this.State = EmulatorState.Ready;
        }
        /// <summary>
        /// Starts or resumes emulation.
        /// </summary>
        public void Run()
        {
            if(this.State == EmulatorState.NoProgram)
                throw new InvalidOperationException("No program has been loaded.");
            if(this.State == EmulatorState.ProgramExited)
                throw new InvalidOperationException("The program has completed.");

            if(this.State == EmulatorState.Ready)
            {
                processorThread = new Thread(ProcessorThreadMain);
                processorThread.IsBackground = true;
                processorThread.Start();
            }
            else if(this.State == EmulatorState.Paused)
                resumeEvent.Set();
        }
        /// <summary>
        /// Pauses emulation.
        /// </summary>
        public void Pause()
        {
            if(this.State != EmulatorState.Running)
                throw new InvalidOperationException("No program is running.");

            pauseEvent.Set();
        }
        /// <summary>
        /// Immediately stops emulation and places the emulator in a halted state.
        /// </summary>
        public void Halt()
        {
            this.State = EmulatorState.Halted;
            if(processorThread != null && processorThread.IsAlive)
            {
                processorThread.Abort();
                processorThread = null;
            }
        }
        /// <summary>
        /// Presses a key on the emulated keyboard.
        /// </summary>
        /// <param name="key">Key to press.</param>
        public void PressKey(Keys key)
        {
            bool pressed;
            if(!keyPressMap.TryGetValue(key, out pressed))
                pressed = false;

            if(!pressed)
            {
                keyPressMap[key] = true;
                vm.PressKey(key);
            }
        }
        /// <summary>
        /// Releases a key on the emulated keyboard.
        /// </summary>
        /// <param name="key">Key to release.</param>
        public void ReleaseKey(Keys key)
        {
            bool pressed;
            if(!keyPressMap.TryGetValue(key, out pressed))
                pressed = false;

            if(pressed)
            {
                keyPressMap[key] = false;
                vm.ReleaseKey(key);
            }
        }
        /// <summary>
        /// Releases all currently pressed keyboard keys.
        /// </summary>
        public void ReleaseAllKeys()
        {
            var pressedKeys = from pair in keyPressMap
                              where pair.Value
                              select pair.Key;

            List<Keys> keysToRelease = new List<Keys>(pressedKeys);
            foreach(var key in keysToRelease)
                ReleaseKey(key);
        }
        /// <summary>
        /// Signals that a mouse input event has occurred.
        /// </summary>
        /// <param name="mouseEvent">Mouse input event that has occurred.</param>
        public void MouseEvent(MouseEvent mouseEvent)
        {
            if(mouseEvent == null)
                throw new ArgumentNullException("mouseEvent");

            mouseQueue.Enqueue(mouseEvent);
        }
        /// <summary>
        /// Releases resources used by the emulator.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Emulates a block of instructions using recompilation.
        /// </summary>
        /// <returns>Number of instructions emulated.</returns>
        private int RecompileInstructions()
        {
            vm.PerformDmaTransfers();
            int count = 0;

            try
            {
                if((vm.Processor.Flags & EFlags.InterruptEnable) != 0)
                    CheckHardwareInterrupts();

                if(this.recompiler == null)
                    this.recompiler = new DynamicRecompiler(vm);

                count = this.recompiler.Run();
            }
            catch(EmulatedException ex)
            {
                if(!vm.RaiseException(ex))
                    throw;
            }

            return count;
        }
        /// <summary>
        /// Emulates a number of instructions.
        /// </summary>
        /// <param name="count">Approximate number of instructions to emulate.</param>
        private void EmulateInstructions(int count)
        {
            vm.PerformDmaTransfers();

            try
            {
                if(((vm.Processor.Flags & EFlags.InterruptEnable) != 0) & !vm.Processor.TemporaryInterruptMask)
                {
                    while(vm.Processor.InPrefix)
                        vm.Emulate();

                    CheckHardwareInterrupts();
                }

                vm.Emulate(count);
            }
            catch(EmulatedException ex)
            {
                if(!vm.RaiseException(ex))
                    throw;
            }
            catch(EnableInstructionTrapException)
            {
                vm.Emulate();
                while(vm.Processor.InPrefix)
                    vm.Emulate();

                if((vm.Processor.Flags & EFlags.Trap) != 0)
                {
                    vm.RaiseInterrupt(1);
                    vm.Processor.Flags &= ~EFlags.Trap;
                }
            }
        }
        /// <summary>
        /// Emulates a number of instructions and logs them.
        /// </summary>
        /// <param name="count">Approximate number of instructions to emulate.</param>
        private void EmulateInstructionsWithLogging(int count)
        {
            vm.PerformDmaTransfers();

            try
            {
                if((vm.Processor.Flags & EFlags.InterruptEnable) != 0)
                {
                    while(vm.Processor.InPrefix)
                        vm.Emulate(this.log);

                    CheckHardwareInterrupts();
                }

                for(int i = 0; i < count; i++)
                    vm.Emulate(this.log);
            }
            catch(EmulatedException ex)
            {
                if(!vm.RaiseException(ex))
                    throw;
            }
            catch(EnableInstructionTrapException)
            {
                vm.Emulate(this.log);
                while(vm.Processor.InPrefix)
                    vm.Emulate(this.log);

                if((vm.Processor.Flags & EFlags.Trap) != 0)
                {
                    vm.RaiseInterrupt(1);
                    vm.Processor.Flags &= ~EFlags.Trap;
                }
            }
        }
        /// <summary>
        /// Checks for raised hardware interrupts and generates a software interrupt
        /// if necessary.
        /// </summary>
        /// <remarks>
        /// This method does not test the interrupt enable flag on the processor.
        /// Verify that the flag is set before calling this method.
        /// </remarks>
        private void CheckHardwareInterrupts()
        {
            if(!RaiseMouseEvent())
            {
                if(!vm.Keyboard.IsHardwareQueueEmpty)
                    vm.InterruptController.RaiseHardwareInterrupt(1);

                int irq = vm.InterruptController.AcknowledgeRequest();
                if(irq >= 0)
                {
                    vm.RaiseInterrupt((byte)irq);

                    if(irq == vm.InterruptController.BaseInterruptVector1 + 1)
                        vm.PrepareForKeyboardHandler();
                }
            }
        }
        /// <summary>
        /// Raises the VideoModeChanged event.
        /// </summary>
        /// <param name="e">Unused EventArgs object.</param>
        private void OnVideoModeChanged(EventArgs e)
        {
            var videoModeChanged = this.VideoModeChanged;
            if(videoModeChanged != null)
                RaiseEvent(videoModeChanged, e);
        }
        /// <summary>
        /// Raises the MouseMoveByEmulator event.
        /// </summary>
        /// <param name="e">Information about the event.</param>
        private void OnMouseMoveByEmulator(MouseMoveEventArgs e)
        {
            var mouseMove = this.MouseMoveByEmulator;
            if(mouseMove != null)
                RaiseEvent(mouseMove, e);
        }
        /// <summary>
        /// Raises the MouseMove event.
        /// </summary>
        /// <param name="e">Information about the event.</param>
        private void OnMouseMove(MouseMoveEventArgs e)
        {
            var mouseMove = this.MouseMove;
            if(mouseMove != null)
                RaiseEvent(mouseMove, e);
        }
        /// <summary>
        /// Raises the MouseVisibilityChanged event.
        /// </summary>
        /// <param name="e">Unused EventArgs object.</param>
        private void OnMouseVisibilityChanged(EventArgs e)
        {
            var visChanged = this.MouseVisibilityChanged;
            if(visChanged != null)
                RaiseEvent(visChanged, e);
        }
        /// <summary>
        /// Raises the CursorVisibilityChanged event.
        /// </summary>
        /// <param name="e">Unused EventArgs object.</param>
        private void OnCursorVisibilityChanged(EventArgs e)
        {
            var visChanged = this.CursorVisibilityChanged;
            if(visChanged != null)
                RaiseEvent(visChanged, e);
        }
        /// <summary>
        /// Raises the CurrentProcessChanged event.
        /// </summary>
        /// <param name="e">Unused EventArgs object.</param>
        private void OnCurrentProcessChanged(EventArgs e)
        {
            var processChanged = this.CurrentProcessChanged;
            if(processChanged != null)
                RaiseEvent(processChanged, e);
        }
        /// <summary>
        /// Raises the StateChanged event.
        /// </summary>
        /// <param name="e">Unused EventArgs object.</param>
        private void OnStateChanged(EventArgs e)
        {
            var stateChanged = this.StateChanged;
            if(stateChanged != null)
                RaiseEvent(stateChanged, e);
        }
        /// <summary>
        /// Raises the Error event.
        /// </summary>
        /// <param name="e">ErrorEventArgs instance containing information about the error.</param>
        private void OnError(ErrorEventArgs e)
        {
            var error = this.Error;
            if(error != null)
                RaiseEvent(error, e);
        }
        /// <summary>
        /// Raises an event.
        /// </summary>
        /// <param name="method">Delegate of event handler.</param>
        /// <param name="e">Arguments to pass to the event handler.</param>
        private void RaiseEvent(Delegate method, EventArgs e)
        {
            var sync = this.EventSynchronizer;
            if(sync == null)
                method.DynamicInvoke(this, e);
            else
                sync.BeginInvoke(method, this, e);
        }
        /// <summary>
        /// Releases resources used by the emulator.
        /// </summary>
        /// <param name="disposing">True indicates method was called from Dispose; false indicates method was called from Finalize.</param>
        private void Dispose(bool disposing)
        {
            if(disposing && !disposed)
            {
                Halt();
                if(pauseEvent != null)
                    pauseEvent.Close();
                if(resumeEvent != null)
                    resumeEvent.Close();
                if(vm != null)
                    vm.Dispose();
                disposed = true;
            }
        }
        /// <summary>
        /// Contains the main processor emulation loop.
        /// </summary>
        private void ProcessorThreadMain()
        {
            while(true)
            {
                try
                {
                    this.State = EmulatorState.Running;
                    EmulationLoop();
                }
                catch(EndOfProgramException)
                {
                    this.State = EmulatorState.ProgramExited;
                    return;
                }
                catch(NotImplementedException ex)
                {
                    this.State = EmulatorState.Halted;
                    OnError(new ErrorEventArgs(ex.Message));
                    return;
                }
                catch(NotSupportedException)
                {
                    this.State = EmulatorState.Halted;
                    OnError(new ErrorEventArgs("Unknown error."));
                    return;
                }
            }
        }
        /// <summary>
        /// Runs the normal CPU emulation loop.
        /// </summary>
        private void EmulationLoop()
        {
            System.Diagnostics.Stopwatch delayTimer = new System.Diagnostics.Stopwatch();

            bool breakPause = false;
            vm.InterruptTimer.Reset();

            while(true)
            {
                long workTicks = emulationSpeed / SpeedDivisors[emulationSpeed - 1] * InterruptTimer.StopwatchTicksPerMillisecond;
                int restTime = (MaximumSpeed - emulationSpeed) / SpeedDivisors[emulationSpeed - 1];

                delayTimer.Reset();
                delayTimer.Start();
                do
                {
                    if(vm.InterruptTimer.IsIntervalComplete)
                    {
                        // Raise the hardware timer interrupt.
                        vm.InterruptController.RaiseHardwareInterrupt(0);
                        vm.InterruptTimer.Reset();
                    }

                    if(this.log == null)
                        EmulateInstructions(500);
                    else
                        EmulateInstructionsWithLogging(500);

                    //var count = RecompileInstructions();

                    Interlocked.Add(ref totalInstructions, 500);
                }
                while(delayTimer.ElapsedTicks < workTicks);

                bool pauseSignal = pauseEvent.WaitOne(restTime);
                if(pauseSignal || breakPause)
                {
                    foreach(var device in vm.Devices)
                        device.Pause();

                    this.State = EmulatorState.Paused;
                    resumeEvent.WaitOne();

                    foreach(var device in vm.Devices)
                        device.Resume();

                    breakPause = false;
                    this.State = EmulatorState.Running;
                }
            }
        }
        /// <summary>
        /// Checks for events in the mouse input queue and raises the next one.
        /// </summary>
        /// <returns>Value indicating whether an event was handled.</returns>
        private bool RaiseMouseEvent()
        {
            MouseEvent mouseEvent;
            if(mouseQueue.TryDequeue(out mouseEvent))
            {
                vm.MouseEvent(mouseEvent);
                return true;
            }
            else
                return false;
        }
        #endregion

        #region Private Fields
        private readonly VirtualMachine vm;
        private Thread processorThread;
        private AutoResetEvent pauseEvent = new AutoResetEvent(false);
        private AutoResetEvent resumeEvent = new AutoResetEvent(false);
        private readonly ConcurrentQueue<MouseEvent> mouseQueue = new ConcurrentQueue<MouseEvent>();
        private EmulatorState currentState;
        private bool disposed;
        private long totalInstructions;
        private Dictionary<Keys, bool> keyPressMap = new Dictionary<Keys, bool>();
        private int emulationSpeed = 18;
        private readonly InstructionLog log;
        private DynamicRecompiler recompiler;
        #endregion

        #region Private Static Fields
        /// <summary>
        /// One-based array of scaling factors for emulation speeds.
        /// </summary>
        private static readonly int[] SpeedDivisors = { 1, 2, 1, 4, 5, 2, 1, 4, 1, 10, 1, 4, 1, 2, 5, 4, 1, 2, 1, 1 };
        #endregion

        #region Public Constants
        /// <summary>
        /// The smallest number that may be assigned to the EmulationSpeed property.
        /// </summary>
        public const int MinimumSpeed = 1;
        /// <summary>
        /// The largest number that may be assigned to the EmulationSpeed property.
        /// </summary>
        public const int MaximumSpeed = 20;
        #endregion
    }

    /// <summary>
    /// Describes the current state of the emulated system.
    /// </summary>
    public enum EmulatorState
    {
        /// <summary>
        /// The emulator is initialized but no program has been loaded yet.
        /// </summary>
        NoProgram,
        /// <summary>
        /// The emulator is ready to run.
        /// </summary>
        Ready,
        /// <summary>
        /// The emulator is running.
        /// </summary>
        Running,
        /// <summary>
        /// The emulator is paused.
        /// </summary>
        Paused,
        /// <summary>
        /// The emulator has reached the end of the currently loaded program.
        /// </summary>
        ProgramExited,
        /// <summary>
        /// The emulator has been halted and cannot be resumed.
        /// </summary>
        Halted
    }

    /// <summary>
    /// Provides information about an error in emulation.
    /// </summary>
    public class ErrorEventArgs : EventArgs
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the ErrorEventArgs class.
        /// </summary>
        /// <param name="message">Message describing the error.</param>
        public ErrorEventArgs(string message)
        {
            this.message = message;
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets a message describing the error.
        /// </summary>
        public string Message
        {
            get { return message; }
        }
        #endregion

        #region Private Fields
        private readonly string message;
        #endregion
    }
}
